// Created Aug. 30, 2007
//
// File: tetris.js
// Author: Nate Young

//global variables section
//
gameloop = false;       //will hold closure to call at intervals
endgame = false;        //will hold id of interval, so it can be cleared

seed = 314;             //seed number for random number generator

board_height = 18;      //height in grid units of board
board_width = 10;       //width in grid units of board
prev_height = 4;        //height in grid units of preview pane
prev_width = 6;         //width in grid units of preview pane

glbl_width = 20;        //width of block
border_width = 3;       //width of border around board
board_top = 40;         //top offset of board
board_left = 40;        //left offset of board
prev_top = 100;         //top offset of preview box
prev_left = 280;        //left offset of preview box
oppo_top = 40;		    //top offset of opponent's board
oppo_left = 440; 	    //left offset of opponent's board
oppo_prev_top = 100;	//top offset of opponent's preview box
oppo_prev_left = 680;	//left offset of opponent's preview box

level = 0;              //keeps track of the player's level
lines = 0;              //number of lines a player has cleared
level_timing = [2, 4, 6, 8, 10, 12, 14, 16, 18, 32, 64];  //number of times per second a block falls

glbl_board = false;     //global board variable
current_piece = false;  //user-manipulable currently-falling piece
next_piece = false;     //the next piece
oppo_board = new Array(); //array of DOM divs
oppo_prev = false;      //array of DOM divs

piecequeue = [];        //holds the sequence of pieces

SQUARE = 0;             //piece types
TEE = 1;
STRAIGHT = 2;
RZEE = 3;
LZEE = 4;
RELL = 5;
LELL = 6;

init_positions = [         //intial positions of blocks, with center block first
                  [[5,17], //square - 0
                   [6,17],
                   [5,16],
                   [6,16]],
                  [[5,17], //tee - 1
                   [4,17],
                   [6,17],
                   [5,16]],
                  [[5,17], //straight - 2
                   [4,17],
                   [6,17],
                   [7,17]],
                  [[5,17], //rzee - 3
                   [6,17],
                   [5,16],
                   [4,16]],
                  [[5,17], //lzee - 4
                   [4,17],
                   [5,16],
                   [6,16]],
                  [[5,17], //rell - 5
                   [4,17],
                   [6,17],
                   [4,16]],
                  [[5,17], //lell - 6
                   [4,17],
                   [6,17],
                   [6,16]]
];

prev_positions = [         //initial positions of preview pieces
                  [[1,2],  //square - 0
                   [2,2],
                   [1,1],
                   [2,1]],
                  [[2,2],  //tee - 1
                   [1,2],
                   [3,2],
                   [2,1]],
                  [[2,2],  //straight - 2
                   [1,2],
                   [3,2],
                   [4,2]],
                  [[2,2],  //rzee - 3
                   [3,2],
                   [1,1],
                   [2,1]],
                  [[2,2],  //lzee - 4
                   [1,2],
                   [2,1],
                   [3,1]],
                  [[2,2],  //rell - 5
                   [1,2],
                   [1,1],
                   [3,2]],
                  [[2,2],  //lell - 6
                   [1,2],
                   [3,2],
                   [3,1]]
];                  

//
//end of global variables section

// Piece class
//
// A piece class holds 4 blocks in a list member
// called, oddly enough, blocks.  As well as
// the type of piece it is.
//
function Piece(type) {
    this.type = type;
    this.blocks = new Array();
}

// add a block to a piece
Piece.prototype.addBlock = function(b) {
	this.blocks.push(b);
}

// construct and return single div element
//
function createblock() {
    var blk = document.createElement("div");
    blk.className = "block";
    blk.style.backgroundColor = "red";

    return blk;
}

// construct and return single div element
//
function creatediv() {
    var div = document.createElement("div");
    div.className = "block";
    div.style.backgroundColor = "blue";

    return div;
}

// construct the divs to display the opponent's preview piece
//
function createOppoPrev() {
    var prev = new Array();
    for(var i = 0; i < 4; i++) {
		var div = creatediv();
		div.style.visibility = "hidden";
		prev.push(div);
		document.body.appendChild(div);
    }
    return prev;
}

// construct a fourblockbattle board
//
function createboard() {
    var board = new Array(board_width);
    for(var i = 0; i < board_width; i++) {
        board[i] = new Array(board_height);
    }

    return board;
}

// sets a set of block-div's top and left CSS properties by converting
// each block's logical board position into its position in the window
//
function translatePos(blocks, top_offset, left_offset, grid_height) {
    for(var i = 0; i < blocks.length; i++) {
        var blk = blocks[i];
        var x = blk.x;
        var y = blk.y;
        blk.style.top = glbl_width * (grid_height - 1 - y) + top_offset;
        blk.style.left = glbl_width * x + left_offset + border_width;
    }
}

// positions a set of divs according to x and y positions given in a set of 
// "blocks" using each div's top and left CSS properties by converting
// each block's logical board position into its position in the window
//
function translatePos2(blocks, divs, top_offset, left_offset, grid_height) {
    var i;
    for(i = 0; i < blocks.length; i++) {
        var blk = blocks[i];
	var div = divs[i];
        var x = blk.x;
        var y = blk.y;
        div.style.top = glbl_width * (grid_height - 1 - y) + top_offset;
        div.style.left = glbl_width * x + left_offset + border_width;
        div.style.visibility = "visible";
    }
}

// draws a piece's blocks on the game board
//
function drawonboard(piece) {
    translatePos(piece.blocks, board_top, board_left, board_height);
}

// draws a piece's blocks in the preview box
//
function drawinpreview(piece) {
    translatePos(piece.blocks, prev_top, prev_left, prev_height);
}

// draw's an opponent's entire board
//
function drawoppoboard(board, divs) {
    translatePos2(board, divs, oppo_top, oppo_left, board_height);
}

// draws an opponent's preview piece
//
function drawoppopreview(piece, divs) {
    translatePos2(piece, divs, oppo_prev_top, oppo_prev_left, prev_height);
}

// checks if a grid position is out of bounds
// or already contains a block
//
function blocklegal(x, y, board) {
    if(x >= board_width || x < 0 || y < 0 || board[x][y])
        return false;
    return true;
}

// checks that all of the x,y pairs in
// positions are legal positions
//
// positions - a list of [x,y] elements
//
function checkposset(positions, board) {
    for(var i = 0; i < positions.length; i++) {
        if(!blocklegal(positions[i][0], positions[i][1], board))
            return false;
    }
    return true;
}

// if the entire set of positions are legal,
// the first four become the positions
// of piece's blocks, and returns true,
// otherwise, false
//
function checkandset(piece, positions, board) {
    if(checkposset(positions, board)) {
        for(var i = 0; i < 4; i++) {
            piece.blocks[i].x = positions[i][0];
            piece.blocks[i].y = positions[i][1];
        }
        return true;
    }
    return false;
}

// tries to move a piece downward
// one grid unit, returns false
// if it can't, true if it can and does
//
function fall(piece, board) {
    function onebelow(blk) {
        return [blk.x, blk.y - 1];
    }
    var fell = checkandset(piece, map(onebelow, piece.blocks), board);

    if(fell)
        drawonboard(piece);

    return fell;
}

// drops a piece as far down as it can
// go instantly, does not return a value
//
function plummet(piece, board) {
    function setpos(blk, pos) {
        blk.x = pos[0];
        //adjusts for incorrect y depth
        blk.y = pos[1] + 1;
    }

    var depth = 0;
    while(true) {
        function nbelow(blk) {
            return [blk.x, blk.y - (depth + 1)];
        }
        var posset = map(nbelow, piece.blocks);
        if(!checkposset(posset, board)) {
            map(setpos, piece.blocks, posset);
            break;
        }
        depth++;
    }
    drawonboard(piece);
}

// transfers a movable piece onto
// an immovable position on the board
//
function settle(piece, board) {
    function mvtoboard(blk) {
        board[blk.x][blk.y] = blk;
    }

    map(mvtoboard, piece.blocks);
}

// moves a piece horizontally
//
// dist - a positive or negative integer distance to move piece
//
function shift(dist, piece, board) {
    function hshift(blk) {
        return [blk.x + dist, blk.y];
    }

    var shifted = checkandset(piece, map(hshift, piece.blocks), board);

    if(shifted)
        drawonboard(piece);
}

// moves a piece one unit left
//
function shiftleft(piece, board) {
    shift(-1, piece, board);
}

// moves a piece one unit right
//
function shiftright(piece, board) {
    shift(1, piece, board);
}

// rotates a piece 90 deg. clockwise
//
function rotatecw(piece, board) {
    if(piece.type == SQUARE) //no need to rotate square pieces
        return;

    // the coordinates around which to rotate the piece
    var aroundx = piece.center.x;
    var aroundy = piece.center.y;

    // check if center is next to the edge of the board
    // which will prevent pieces from rotating
    // this technique does not work flawlessly for straight pieces
    // so it checks that as well
    if(((aroundx + 1 >= board_width) ||
        (aroundx - 1 < 0)) && (piece.type != STRAIGHT))
        aroundx = aroundx - 1 < 0 ? aroundx + 1 : aroundx - 1;

    // to rotate a piece 90 deg. clockwise
    // its position becomes its offset from
    // center [x,y] reversed [y,x] and the
    // x component negated [y, -x]
    function rotcoord(blk) {
        var relx = blk.x - aroundx;
        var rely = blk.y - aroundy;

        return [aroundx + rely, aroundy - relx];
    }

    var rotated = checkandset(piece, map(rotcoord, piece.blocks), board);

    if(rotated)
        drawonboard(piece);
}

// rotates a piece 90 deg. counterclockwise
//
function rotateccw(piece, board) {
    if(piece.type == SQUARE) // no need to rotate SQUARE pieces
        return;

    // coordinates around which to rotate
    var aroundx = piece.center.x;
    var aroundy = piece.center.y;

    // check for center next to board edge
    if(((aroundx + 1 >= board_width) ||
        (aroundx - 1 < 0)) && (piece.type != STRAIGHT))
        aroundx = aroundx - 1 < 0 ? aroundx + 1 : aroundx - 1;

    // ccw rotate function: [x,y] from center
    // becomes [-y, x]
    function rotcoord(blk) {
        var relx = blk.x - aroundx;
        var rely = blk.y - aroundy;

        return [aroundx - rely, aroundy + relx];
    }

    var rotated = checkandset(piece, map(rotcoord, piece.blocks), board);

    if(rotated)
        drawonboard(piece);
}

// checks the entire board for any 
// completed lines and executes the
// given handler for it
//
function fulline(board, clearfn) {
    var c;
    var toclear = new Array();
    for(var r = 0; r < board_height; r++) {
        for(c = 0; c < board_width; c++) {
            if(!board[c][r])
                break;
        }
        if(c >= board_width) {
            toclear.push(r);
        }
    }
    clearfn(toclear, board);
}

// moves all blocks above the given row down by one unit
//
function dropabove(row, board) {
    // collect all blocks above row
    function findblk(blk) {
        return blk ? blk.y > row : false;
    }
    var allabove = rfilter(findblk, board);

    // drop all by one unit
    function boarddown(blk) {
        board[blk.x][blk.y] = false;
        blk.y -= 1;
        board[blk.x][blk.y] = blk;
    }
    map(boarddown, allabove);

    // draw the newly positioned blocks
    translatePos(allabove, board_top, board_left, board_height);
}

// increases the games level by one
// right now this just means that blocks fall faster
// as configured in the level_timing global variable
//
function nextlevel() {
    level++;
    clearInterval(endgame);
    endgame = setInterval("gameloop()", 1000/level_timing[level]);
}

// given a list of rows to clear, 
// removes each from the board
//
function clearallines(rows, board) {
    //clear from the top down
    rows.sort(function(a,b){return b - a;});  //sort into reverse order
    for(var r = 0; r < rows.length; r++)
        clearline(rows[r], board);
}

function clearline(row, board) { 
    // increment number of lines cleared, update page to reflect
    lines++;
    updatelines(lines);

    // remove blocks from board
    for(var col = 0; col < board_width; col++) {
        var blk = board[col][row]
        board[col][row] = false;
        document.body.removeChild(blk);
    }

    dropabove(row, board);

    // level up every 10 lines cleared
    if(lines > 1 && lines % 10 == 0)
        nextlevel();
}

// returns a random Piece object
//
function nextpiece(fn) {
    var p = RandomPiece.nextPiece(fn);
}

// tries to put a piece's blocks in their initial positions
// at the top of the board.  Will return false if not 
// possible (anothe piece in the way), true otherwise
//
function initialplace(piece, board) {
    //get initial positions for piece type
    var blkpos = init_positions[piece.type];

    //check that positions are available
    if(!checkposset(blkpos, board))
        return false;
    
    function setblkpos(blk, pos) {
        blk.x = pos[0];
        blk.y = pos[1];
    }

    map(setblkpos, piece.blocks, blkpos);

    drawonboard(piece);

    return true;
}

function initialplace2(piece, board, unplaceable) {
    //get initial positions for piece type
    var blkpos = init_positions[piece.type];

    //check that positions are available
    if(!checkposset(blkpos, board))
        unplaceable();
    
    function setblkpos(blk, pos) {
        blk.x = pos[0];
        blk.y = pos[1];
    }

    map(setblkpos, piece.blocks, blkpos);

    drawonboard(piece);
}

// gets the next piece,
// creates its blocks
// draws them in the preview box
// and returns the piece
//
function previewplace() {
    //get next piece from random sequence
    var piece = nextpiece();

    //get preview positions for piece type
    var blkpos = prev_positions[piece.type];

    function blockcreate(pos) {
        var blk = createblock();
        blk.x = pos[0];
        blk.y = pos[1];
        piece.addBlock(blk);
        document.body.appendChild(blk);
    }

    //map creation function across initial positions
    map(blockcreate, blkpos);
    piece.center = piece.blocks[0];

    drawinpreview(piece);

    return piece;
}

function previewplace2(type) {
	var piece = new Piece(type);

    //get preview positions for piece type
    var blkpos = prev_positions[piece.type];

    function blockcreate(pos) {
        var blk = createblock();
        blk.x = pos[0];
        blk.y = pos[1];
        piece.addBlock(blk);
        document.body.appendChild(blk);
    }

    //map creation function across initial positions
    map(blockcreate, blkpos);
    piece.center = piece.blocks[0];

    drawinpreview(piece);
}

// reports the current state of the player's board
//
function reportboard(board, current, preview) {
    function extractblk(fullblk) {
	return fullblk;
    }
    var boardblks = rfilter(extractblk, board);
    boardblks = boardblks.concat(current.blocks);

    function pullpos(blk) {
	return {x: blk.x, y: blk.y};
    }

    boardblks = map(pullpos, boardblks);

    var prevblks = map(pullpos, preview.blocks);

    sendboard(boardblks, prevblks);
}

// passes the player's board and preview piece data
// on to a fourblockbattle server
//
function sendboard(board, prev) {
    var paramstr = "board=";
	paramstr += JSON.stringify(board);
	paramstr += "&prev=";
	paramstr += JSON.stringify(prev);
	postRequest("sendboard", function(xhr) { }, paramstr);

    tmpboard = board;
    tmpprev = prev;
}

function oppoboardHandler(httprequest) {
	var boardandprev = eval(httprequest.responseText);
	var board = boardandprev[0];
	var prev = boardandprev[1];
	refreshoppo(board, prev);
}


// retreives the state of an opponent's board from
// a fourblockbattle server
//
function getoppoboard() {
	makeRequest("oppoboard", oppoboardHandler);
}

// draws an opponent's board on the screen
//
function refreshoppo(boardblks, prevblks) {
    //check size of boarddivs, and create and append extra divs to oppo_board if necessary
    if(boardblks.length > oppo_board.length) {
		var diff = (boardblks.length - oppo_board.length);
		for(var i = 0; i < diff; i++) {
		    var div = creatediv();
		    oppo_board.push(div);
		    document.body.appendChild(div);
		}
    }

    drawoppoboard(boardblks, oppo_board);
    //hide the pieces that weren't drawn
    for(var i = boardblks.length; i < oppo_board.length; i++) {
        oppo_board[i].style.visibility = "hidden";
    }

    drawoppopreview(prevblks, oppo_prev);
}

function opponentArrive(opponame) {
	var nameslot = document.getElementById("opponame");
	nameslot.replaceChild(document.createTextNode(opponame), nameslot.firstChild);
	startgame();
}

// main gameloop, called on load of the document body
//
// creates initial structures, gameboard and two pieces
// sets up key handlers to control the pieces,
// and then sets the falloop() local function to be called
// at a certain interval corresponding to level 0 of timing.
//
// the falloop drops the current piece until it can't anymore
// (fall() returns false), moves the piece onto the board
// and checks if any full lines should be cleared
// it then moves the piece sitting in the preview box onto the
// board and puts a new piece in the preview box
//
function startgame() {
    glbl_board = createboard();
    nextpiece(function(type) {
    	var piece = new Piece(type);
    	initialplace2(piece, glbl_board, gameover);
    });
    nextpiece(previewplace2);
    oppo_prev = createOppoPrev();

    document.onkeydown = function(event) {
        var code = window.event ? window.event.keyCode : event.which;
        arrowkeymap(code);
    };

    function falloop() {
        if(!fall(current_piece, glbl_board)) {
            settle(current_piece, glbl_board);
            fulline(glbl_board, clearallines);
            current_piece = next_piece;
            if(!initialplace(current_piece, glbl_board)) { //if this fails, the player loses
                gameover();
                return;
            }
            next_piece = previewplace();
        }
		reportboard(glbl_board, current_piece, next_piece);
		getoppoboard();
    }

    gameloop = falloop;
    
    endgame = setInterval("gameloop()", 1000/level_timing[0]);
}

// action to take on end of game
// 
// this simple function stops the main loop
// and displays the message "Game Over."
//
function gameover() {
    clearInterval(endgame);
    alert("Game Over.");
}

// a map of actions to take based on 
// a given keycode.
//
// this function moves and rotates a  piece with the
// arrow keys and plummets a piece with the spacebar
//
function arrowkeymap(keycode) {
    //shift left
    if(keycode == 37) //left arrow
        shiftleft(current_piece, glbl_board);

    //shift right
    if(keycode == 39) //right arrow
        shiftright(current_piece, glbl_board);

    //fall
    if(keycode == 40) //down arrow
        fall(current_piece, glbl_board);

    //plummet
    if(keycode == 32) //space
        plummet(current_piece, glbl_board);

    //rotate clockwise
    if(keycode == 38) //up arrow
        rotatecw(current_piece, glbl_board);
}

// changes the displayed number of lines cleared
//
function updatelines(lines) {
    var span = document.getElementById("lines");
    var text = document.createTextNode("" + lines);
    span.replaceChild(text, span.firstChild);
}
